fix(dashboard): stop the shell scrolling sideways between 768px and ~890px - #588
Merged
Merged
Conversation
…890px Every `/dashboard/*` page could only be read by panning horizontally at viewports between 768px and roughly 890px. Measured on the student dashboard, the document was 87px wider than the viewport at 768px, 55px at 800px and 5px at 850px. Two things collided, and both needed fixing. `SidebarInset` was `w-full flex-1` with no `min-w-0`. As a flex item its min-width resolved to `auto`, so it refused to shrink below its min-content width and widened the document instead of letting the offending content clip or scroll inside itself. That is a latent trap for any wide child — tables, code blocks, charts — not only for the case found here. The content that tripped it was the gamification header card, gated on `hidden md:block`. The desktop sidebar also appears at `md` and takes 256px, so the card arrived at exactly the moment the column it lives in got 256px narrower. Teacher and admin dashboards, which render the same shell without that card, never overflowed at any width — which is what isolated it. The card now waits until `lg`. Between 768px and 1023px it is still reachable in the avatar dropdown, whose gate moved from `md:hidden` to `lg:hidden` in lockstep; the two breakpoints are a pair, and a test now asserts the card is visible in exactly one of the two places at every width rather than in neither. Losing the always-visible chips in that band is a deliberate trade against a page that cannot be read without panning. Also matched the card's loading skeleton to its loaded state (`max-w-full overflow-hidden`), since the skeleton was briefly wider than the thing it stood in for. `tests/playwright/specs/overflow-586.spec.ts` walks student, teacher and admin dashboards across 700/768/800/850/900/1024/1280 and asserts `scrollWidth === clientWidth` everywhere. It fails on the parent commit with exactly the numbers above. Closes #586 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01183C2p5ed7youHQAUdPfR1
Screenshots at 768px and 820px on the student dashboard and courses list, plus a scrolled-fully-right frame showing what was unreachable before, and a pair of GIFs sweeping the viewport from 700px to 1024px. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01183C2p5ed7youHQAUdPfR1
guillermoscript
marked this pull request as ready for review
July 28, 2026 19:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Adds
min-w-0toSidebarInsetand moves the student gamification header card from themdbreakpoint tolg(in lockstep with its avatar-dropdown fallback), so no/dashboard/*page scrolls horizontally any more.Closes #586
Why
Between 768px and roughly 890px, every dashboard page could only be read by panning sideways — the right edge of the content was simply unreachable. Two independent defects combined to cause it, and both are fixed here.
1.
SidebarInsetcould not shrink.components/ui/sidebar.tsxhad it asrelative flex w-full flex-1 flex-col …with nomin-w-0. As a flex item itsmin-widthresolves toauto, so it refuses to go below its min-content width and widens the whole document rather than letting the offending child clip or scroll inside itself. This is a latent trap for any wide descendant — tables, code blocks, charts — not only for the case found here, and it applies to/platform/*too since that shell uses the same component.2. The gamification card turned on at the same breakpoint as the sidebar. The desktop sidebar appears at
mdand claims 256px; the card was gated onhidden md:block, so it arrived at the exact moment its column got 256px narrower. Its intrinsic width (ashrink-0Level block plus a fixedw-20/w-24XP bar) plus the locale, theme and avatar controls then exceeded what was left.Measuring teacher and admin dashboards is what isolated the second cause. They render the identical shell, with the identical 256px sidebar, and never overflowed at any width — the only difference is that they have no gamification card:
/dashboard/student/dashboard/student/courses/dashboard/student/browse/dashboard/teacher/dashboard/admin(px of
documentElement.scrollWidth - clientWidth, on unmodifiedmaster. Every cell is 0 after this change.)Fixing only the first leaves the card cramped into a column too narrow for it; fixing only the second leaves the shell fragile for the next wide thing anyone adds. Hence both.
The trade worth reviewing
Between 768px and 1023px the streak/coins/level chips are no longer always visible in the header. They move to the avatar dropdown, which already carried them as the small-screen fallback — its gate moved from
md:hiddentolg:hiddenin the same commit. Those two breakpoints are now a matched pair, and if they ever drift apart the chips would silently vanish (or double up) in the gap, so there is a test asserting the card is visible in exactly one of the two places at every width.Losing glanceable chips in a 255px-wide band is, I think, clearly better than a page that cannot be read without panning — but it is a product decision, so flag it if you disagree. The alternative considered was letting the chips wrap or truncate in place; that keeps them visible but makes the header two rows tall in the same band, which felt worse.
Also matched the card's loading skeleton to its loaded state (
max-w-full overflow-hidden) — the skeleton was briefly wider than the thing it stood in for.How to QA
On a fresh
npm run db:reset, withnpm run devrunning. Account:student@e2etest.com/password123ondefault.lvh.me:3000./en/dashboard/student.masterthe sideways scroll shows up as soon as you cross below ~890px and is worst at exactly 768px.)/en/dashboard/teacherand/en/dashboard/admin(owner@e2etest.com) to confirm nothing regressed on the shells that were already fine.Automated, and the reason this is worth running rather than just reading:
tests/playwright/specs/overflow-586.spec.tswalks student, teacher and admin dashboards across 700/768/800/850/900/1024/1280 and assertsscrollWidth === clientWidthat every combination, plus the header/dropdown lockstep invariant. It fails on the parent commit with exactly the numbers in the table above — it is a regression test, not a smoke test. (Note: if your.env.localsetsE2E_BASE_URLto a non-3000 port, override it to match your dev server.)Screenshots / GIF
Viewport swept from 700px to 1024px on
/dashboard/student.Before — the layout jumps and the page scrolls sideways across the 768–890px band:
After — the same sweep, no horizontal scroll at any point; the tail of the clip opens the avatar dropdown at 820px to show the chips are still reachable there:
Stills at 768px,
/dashboard/student— before, and the same page scrolled fully right to show what was unreachable, then after:/dashboard/student/coursesat 768px, and/dashboard/studentat 820px:Checklist
npm run typecheckandnpm run test:unitpass — typecheck clean, 675 unit tests across 54 files passnpm run buildpassestenant_id(or the table genuinely has no such column) — no queries added; this is CSS-class and layout-gate onlymessages/en.jsonandmessages/es.json— n/a, no new stringsnpm run db:reset, has RLS policies, andlib/database.types.tswas regenerated — n/a, no migrationOne thing to be aware of when reviewing
min-w-0onSidebarInsetis global — it changes every dashboard and platform page, not just the ones in the table. The failure mode it introduces is the opposite one: content that used to widen the page will now clip or scroll within the inset instead. That is the correct behaviour, and the seven-width sweep across five pages is there to catch anything that reflows badly, but it is a shared shell component and a wider eye on it would be welcome.